@rebasepro/admin 0.13.1-canary.gf57a27e → 0.14.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-BnfOcqM_.js → CollectionEditorDialog-CSheud_E.js} +19 -9
- package/dist/CollectionEditorDialog-CSheud_E.js.map +1 -0
- package/dist/{PropertyEditView-vLXglzIy.js → PropertyEditView-CWZlC1gq.js} +5 -5
- package/dist/PropertyEditView-CWZlC1gq.js.map +1 -0
- package/dist/{RouterCollectionsStudioView-DF3aiOuv.js → RouterCollectionsStudioView-CwTpdzWF.js} +4 -4
- package/dist/{RouterCollectionsStudioView-DF3aiOuv.js.map → RouterCollectionsStudioView-CwTpdzWF.js.map} +1 -1
- package/dist/collection_editor_ui.js +4 -4
- package/dist/components/ClearFilterSortButton.d.ts +7 -1
- package/dist/components/CollectionViewBinding/SortButton.d.ts +7 -1
- package/dist/components/RelationSelector.d.ts +13 -0
- package/dist/data_export/export/export.d.ts +7 -0
- package/dist/data_export/export/fetch_export_data.d.ts +48 -0
- package/dist/data_export/export/index.d.ts +1 -0
- package/dist/data_import/utils/csv.d.ts +30 -0
- package/dist/data_import/utils/index.d.ts +2 -0
- package/dist/data_import/utils/save_entities.d.ts +38 -0
- package/dist/data_import/utils/transforms.d.ts +9 -0
- package/dist/editor.js +3 -0
- package/dist/editor.js.map +1 -1
- package/dist/{export-C26xQN-g.js → export-L53WektO.js} +175 -39
- package/dist/export-L53WektO.js.map +1 -0
- package/dist/{history-DSS-Ebwh.js → history-D5SKygpJ.js} +2 -2
- package/dist/{history-DSS-Ebwh.js.map → history-D5SKygpJ.js.map} +1 -1
- package/dist/{import-Dhx96B6x.js → import-C_4edkoD.js} +5 -4
- package/dist/import-C_4edkoD.js.map +1 -0
- package/dist/index.js +93 -74
- package/dist/index.js.map +1 -1
- package/dist/preview/components/date_fns_locales.d.ts +9 -0
- package/dist/{util-Bt6VadeZ.js → util-C-D5yD2n.js} +567 -113
- package/dist/util-C-D5yD2n.js.map +1 -0
- package/package.json +10 -9
- package/src/collection_editor/ui/collection_editor/AICollectionGeneratorPopover.tsx +8 -3
- package/src/collection_editor/ui/collection_editor/CollectionDetailsForm.tsx +5 -2
- package/src/collection_editor/ui/collection_editor/CollectionJsonImportDialog.tsx +1 -0
- package/src/collection_editor/ui/collection_editor/CollectionRLSTab.tsx +9 -8
- package/src/collection_editor/ui/collection_editor/GeneralSettingsForm.tsx +5 -2
- package/src/collection_editor/ui/collection_editor/properties/MapPropertyField.tsx +1 -1
- package/src/collection_editor/ui/collection_editor/properties/advanced/AdvancedPropertyValidation.tsx +5 -2
- package/src/collection_editor/ui/collection_editor/properties/conditions/ConditionsEditor.tsx +1 -0
- package/src/collection_editor/useJsonCollectionsConfigController.tsx +92 -84
- package/src/collection_editor/useLocalCollectionsConfigController.tsx +10 -3
- package/src/components/ClearFilterSortButton.tsx +16 -7
- package/src/components/CollectionTableBinding/table_bindings.tsx +7 -1
- package/src/components/CollectionViewBinding/CollectionBoardViewBinding.tsx +13 -7
- package/src/components/CollectionViewBinding/CollectionViewBinding.tsx +1 -0
- package/src/components/CollectionViewBinding/CollectionViewStartActions.tsx +9 -0
- package/src/components/CollectionViewBinding/SortButton.tsx +16 -4
- package/src/components/CollectionViewBinding/ViewModeToggle.tsx +10 -5
- package/src/components/CollectionViewBinding/useBoardDataController.tsx +11 -3
- package/src/components/DefaultDrawer.tsx +7 -0
- package/src/components/DrawerNavigationItem.tsx +12 -0
- package/src/components/RelationSelector.tsx +52 -5
- package/src/components/SearchIconsView.tsx +14 -33
- package/src/components/SelectableTable/filters/StringNumberFilterField.tsx +1 -0
- package/src/components/UserSelector.tsx +47 -5
- package/src/data_export/export/ExportCollectionAction.tsx +101 -48
- package/src/data_export/export/export.ts +63 -16
- package/src/data_export/export/fetch_export_data.ts +90 -0
- package/src/data_export/export/index.ts +1 -0
- package/src/data_import/components/DataNewPropertiesMapping.tsx +20 -3
- package/src/data_import/components/ImportNewPropertyFieldPreview.tsx +1 -0
- package/src/data_import/components/ImportSaveInProgress.tsx +35 -36
- package/src/data_import/import/ImportCollectionAction.tsx +13 -2
- package/src/data_import/utils/csv.ts +153 -0
- package/src/data_import/utils/data.ts +7 -1
- package/src/data_import/utils/file_to_json.ts +77 -6
- package/src/data_import/utils/index.ts +2 -0
- package/src/data_import/utils/save_entities.ts +138 -0
- package/src/data_import/utils/transforms.ts +23 -0
- package/src/editor/components/image-bubble.tsx +2 -0
- package/src/editor/selectors/link-selector.tsx +1 -0
- package/src/form/field_bindings/BinaryFieldBinding.tsx +1 -0
- package/src/form/field_bindings/DateTimeFieldBinding.tsx +4 -0
- package/src/form/field_bindings/KeyValueFieldBinding.tsx +3 -0
- package/src/form/field_bindings/TextFieldBinding.tsx +18 -0
- package/src/form/field_bindings/VectorFieldBinding.tsx +1 -0
- package/src/preview/components/DatePreview.tsx +15 -2
- package/src/preview/components/ReferencePreview.tsx +5 -8
- package/src/preview/components/RelationPreview.tsx +8 -7
- package/src/preview/components/date_fns_locales.ts +125 -0
- package/dist/CollectionEditorDialog-BnfOcqM_.js.map +0 -1
- package/dist/PropertyEditView-vLXglzIy.js.map +0 -1
- package/dist/export-C26xQN-g.js.map +0 -1
- package/dist/import-Dhx96B6x.js.map +0 -1
- package/dist/util-Bt6VadeZ.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.14.0",
|
|
5
5
|
"description": "Rebase CMS — content management views, forms, and routing",
|
|
6
6
|
"funding": {
|
|
7
7
|
"url": "https://github.com/sponsors/rebaseco"
|
|
@@ -83,14 +83,14 @@
|
|
|
83
83
|
"react-dropzone": "^19.1.1",
|
|
84
84
|
"react-use-measure": "^2.1.7",
|
|
85
85
|
"zod": "^4.4.3",
|
|
86
|
-
"@rebasepro/
|
|
87
|
-
"@rebasepro/
|
|
88
|
-
"@rebasepro/
|
|
89
|
-
"@rebasepro/inference": "0.
|
|
90
|
-
"@rebasepro/
|
|
91
|
-
"@rebasepro/
|
|
92
|
-
"@rebasepro/
|
|
93
|
-
"@rebasepro/
|
|
86
|
+
"@rebasepro/admin-types": "0.14.0",
|
|
87
|
+
"@rebasepro/app": "0.14.0",
|
|
88
|
+
"@rebasepro/forms": "0.14.0",
|
|
89
|
+
"@rebasepro/inference": "0.14.0",
|
|
90
|
+
"@rebasepro/types": "0.14.0",
|
|
91
|
+
"@rebasepro/utils": "0.14.0",
|
|
92
|
+
"@rebasepro/common": "0.14.0",
|
|
93
|
+
"@rebasepro/ui": "0.14.0"
|
|
94
94
|
},
|
|
95
95
|
"peerDependencies": {
|
|
96
96
|
"react": ">=19.2.7",
|
|
@@ -107,6 +107,7 @@
|
|
|
107
107
|
"@types/react-dom": "^19.2.3",
|
|
108
108
|
"@types/react-measure": "^2.0.12",
|
|
109
109
|
"@vitejs/plugin-react": "^6.0.4",
|
|
110
|
+
"fast-check": "^4.9.0",
|
|
110
111
|
"jest": "^30.4.2",
|
|
111
112
|
"react-router": "^8.3.0",
|
|
112
113
|
"ts-jest": "^29.4.12",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
|
|
2
|
-
import { AIIcon } from "@rebasepro/app";
|
|
2
|
+
import { AIIcon, useTranslation } from "@rebasepro/app";
|
|
3
3
|
import { useCollectionRegistryController } from "../../_cms_internals";
|
|
4
4
|
import React, { useState } from "react";
|
|
5
5
|
import { useSafeSnackbarController } from "../../useSafeSnackbarController";
|
|
@@ -70,6 +70,7 @@ export function AICollectionGeneratorPopover({
|
|
|
70
70
|
showLabel = true,
|
|
71
71
|
onAnalyticsEvent
|
|
72
72
|
}: AICollectionGeneratorPopoverProps) {
|
|
73
|
+
const { t } = useTranslation();
|
|
73
74
|
const [menuOpen, setMenuOpen] = useState(false);
|
|
74
75
|
const [prompt, setPrompt] = useState("");
|
|
75
76
|
const [loading, setLoading] = useState(false);
|
|
@@ -204,8 +205,8 @@ export function AICollectionGeneratorPopover({
|
|
|
204
205
|
|
|
205
206
|
<Typography variant="caption" color="secondary">
|
|
206
207
|
{existingCollection
|
|
207
|
-
? "
|
|
208
|
-
: "
|
|
208
|
+
? t("describe_changes_to_make")
|
|
209
|
+
: t("describe_collection_to_create")
|
|
209
210
|
}
|
|
210
211
|
</Typography>
|
|
211
212
|
|
|
@@ -213,6 +214,10 @@ export function AICollectionGeneratorPopover({
|
|
|
213
214
|
size="small"
|
|
214
215
|
multiline
|
|
215
216
|
autoFocus
|
|
217
|
+
aria-label={existingCollection
|
|
218
|
+
? t("describe_changes_to_make")
|
|
219
|
+
: t("describe_collection_to_create")
|
|
220
|
+
}
|
|
216
221
|
className="w-full text-text-primary dark:text-text-primary-dark"
|
|
217
222
|
value={prompt}
|
|
218
223
|
onChange={(e) => setPrompt(e.target.value)}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { IconForView } from "@rebasepro/app";
|
|
3
3
|
import { FieldCaption } from "../../_cms_internals";
|
|
4
4
|
import React, { useMemo, useState } from "react";
|
|
5
|
-
import { useAuthController, useCustomizationController } from "@rebasepro/app";
|
|
5
|
+
import { useAuthController, useCustomizationController, useTranslation } from "@rebasepro/app";
|
|
6
6
|
import { getFieldConfig, PropertyConfigBadge, SearchIconsView } from "../../_cms_internals";
|
|
7
7
|
import { Property } from "@rebasepro/types";
|
|
8
8
|
import {
|
|
@@ -372,10 +372,13 @@ function DefaultDatabaseField({
|
|
|
372
372
|
onDatabaseIdUpdate
|
|
373
373
|
}: { databaseId?: string, onDatabaseIdUpdate: (databaseId: string) => void }) {
|
|
374
374
|
|
|
375
|
-
|
|
375
|
+
const { t } = useTranslation();
|
|
376
|
+
|
|
377
|
+
return <Tooltip title={t("database_id")}
|
|
376
378
|
side={"top"}
|
|
377
379
|
align={"start"}>
|
|
378
380
|
<TextField size={"small"}
|
|
381
|
+
aria-label={t("database_id")}
|
|
379
382
|
invisible={true}
|
|
380
383
|
inputClassName={"text-end"}
|
|
381
384
|
value={databaseId ?? ""}
|
|
@@ -40,7 +40,7 @@ import {
|
|
|
40
40
|
} from "@rebasepro/ui";
|
|
41
41
|
import { useFormex } from "@rebasepro/forms";
|
|
42
42
|
import { useCollectionsConfigController } from "../../useCollectionsConfigController";
|
|
43
|
-
import { useRebaseContext } from "@rebasepro/app";
|
|
43
|
+
import { useRebaseContext, useTranslation } from "@rebasepro/app";
|
|
44
44
|
import type { AdminCollection } from "@rebasepro/admin-types";
|
|
45
45
|
import type { PostgresPolicy, SecurityOperation, SecurityRule } from "@rebasepro/types";
|
|
46
46
|
|
|
@@ -394,6 +394,7 @@ function InlinePolicyEditor({
|
|
|
394
394
|
onSave: (policyData: Partial<PostgresPolicy>) => void;
|
|
395
395
|
onCancel: () => void;
|
|
396
396
|
}) {
|
|
397
|
+
const { t } = useTranslation();
|
|
397
398
|
const [name, setName] = useState(policy?.policyname || "");
|
|
398
399
|
const [behavior, setBehavior] = useState<"PERMISSIVE" | "RESTRICTIVE">(policy?.permissive || "PERMISSIVE");
|
|
399
400
|
const [command, setCommand] = useState<PolicyCommand>((policy?.cmd as PolicyCommand) || "ALL");
|
|
@@ -441,8 +442,8 @@ function InlinePolicyEditor({
|
|
|
441
442
|
<Paper className={cls("p-4 md:p-6 flex flex-col gap-6 bg-white dark:bg-surface-800 border-none sm:border-solid rounded-none sm:rounded-xl", defaultBorderMixin)}>
|
|
442
443
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
443
444
|
<div className="flex flex-col gap-1.5">
|
|
444
|
-
<Typography variant="caption" className="uppercase tracking-wider text-text-secondary">
|
|
445
|
-
<TextField value={name} onChange={(e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => setName(e.target.value)} placeholder="e.g. allow_read_all"/>
|
|
445
|
+
<Typography variant="caption" className="uppercase tracking-wider text-text-secondary">{t("studio_policy_name")}</Typography>
|
|
446
|
+
<TextField aria-label={t("studio_policy_name")} value={name} onChange={(e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => setName(e.target.value)} placeholder="e.g. allow_read_all"/>
|
|
446
447
|
</div>
|
|
447
448
|
<div className="flex flex-col gap-1.5">
|
|
448
449
|
<Typography variant="caption" className="uppercase tracking-wider text-text-secondary">Behavior</Typography>
|
|
@@ -484,7 +485,7 @@ function InlinePolicyEditor({
|
|
|
484
485
|
</Typography>
|
|
485
486
|
)}
|
|
486
487
|
<div className="flex gap-2 items-center">
|
|
487
|
-
<TextField size="small" value={customRole}
|
|
488
|
+
<TextField size="small" aria-label="Add a custom role" value={customRole}
|
|
488
489
|
onChange={(e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => setCustomRole(e.target.value)}
|
|
489
490
|
onKeyDown={(e: React.KeyboardEvent) => {
|
|
490
491
|
if (e.key === "Enter") {
|
|
@@ -501,14 +502,14 @@ function InlinePolicyEditor({
|
|
|
501
502
|
</div>
|
|
502
503
|
{command !== "INSERT" && (
|
|
503
504
|
<div className="flex flex-col gap-1.5">
|
|
504
|
-
<Typography variant="caption" className="uppercase tracking-wider text-text-secondary">
|
|
505
|
-
<TextField value={usingExpr} onChange={(e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => setUsingExpr(e.target.value)} placeholder="e.g. auth.uid() = uid"/>
|
|
505
|
+
<Typography variant="caption" className="uppercase tracking-wider text-text-secondary">{t("studio_policy_using_expr")}</Typography>
|
|
506
|
+
<TextField aria-label={t("studio_policy_using_expr")} value={usingExpr} onChange={(e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => setUsingExpr(e.target.value)} placeholder="e.g. auth.uid() = uid"/>
|
|
506
507
|
</div>
|
|
507
508
|
)}
|
|
508
509
|
{showCheck && (
|
|
509
510
|
<div className="flex flex-col gap-1.5">
|
|
510
|
-
<Typography variant="caption" className="uppercase tracking-wider text-text-secondary">
|
|
511
|
-
<TextField value={checkExpr} onChange={(e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => setCheckExpr(e.target.value)} placeholder="e.g. auth.uid() = uid"/>
|
|
511
|
+
<Typography variant="caption" className="uppercase tracking-wider text-text-secondary">{t("studio_policy_check_expr")}</Typography>
|
|
512
|
+
<TextField aria-label={t("studio_policy_check_expr")} value={checkExpr} onChange={(e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => setCheckExpr(e.target.value)} placeholder="e.g. auth.uid() = uid"/>
|
|
512
513
|
</div>
|
|
513
514
|
)}
|
|
514
515
|
</Paper>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
|
|
2
|
-
import { IconForView } from "@rebasepro/app";
|
|
2
|
+
import { IconForView, useTranslation } from "@rebasepro/app";
|
|
3
3
|
import { FieldCaption } from "../../_cms_internals";
|
|
4
4
|
import React, { useState } from "react";
|
|
5
5
|
|
|
@@ -271,10 +271,13 @@ function DefaultDatabaseField({
|
|
|
271
271
|
onDatabaseIdUpdate
|
|
272
272
|
}: { databaseId?: string, disabled?: boolean, onDatabaseIdUpdate: (databaseId: string) => void }) {
|
|
273
273
|
|
|
274
|
-
|
|
274
|
+
const { t } = useTranslation();
|
|
275
|
+
|
|
276
|
+
return <Tooltip title={t("database_id")}
|
|
275
277
|
side={"top"}
|
|
276
278
|
align={"start"}>
|
|
277
279
|
<TextField size={"small"}
|
|
280
|
+
aria-label={t("database_id")}
|
|
278
281
|
invisible={true}
|
|
279
282
|
inputClassName={"text-end"}
|
|
280
283
|
disabled={disabled}
|
|
@@ -111,7 +111,7 @@ export function MapPropertyField({ disabled, getData, allowDataInference, proper
|
|
|
111
111
|
position={"start"}
|
|
112
112
|
size={"medium"}
|
|
113
113
|
label={t("spread_children_as_columns")}
|
|
114
|
-
onValueChange={(v) => setFieldValue("
|
|
114
|
+
onValueChange={(v) => setFieldValue("admin.spreadChildren", v)}
|
|
115
115
|
value={values.admin?.spreadChildren ?? false}
|
|
116
116
|
/>
|
|
117
117
|
<FieldCaption>
|
|
@@ -7,8 +7,11 @@ export function AdvancedPropertyValidation({ disabled }: {
|
|
|
7
7
|
disabled: boolean
|
|
8
8
|
}) {
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
// `ui` was renamed to `admin` in 0.11. Nothing reads `ui.*` — the toggles did
|
|
11
|
+
// nothing — and the boot validator treats the old name as a removed key, so a
|
|
12
|
+
// save through here left a project that would not start.
|
|
13
|
+
const hideFromCollection = "admin.hideFromCollection";
|
|
14
|
+
const readOnly = "admin.readOnly";
|
|
12
15
|
|
|
13
16
|
return (
|
|
14
17
|
|
|
@@ -64,6 +64,24 @@ export function useJsonCollectionsConfigController({
|
|
|
64
64
|
const storeRef = useRef(store);
|
|
65
65
|
storeRef.current = store;
|
|
66
66
|
|
|
67
|
+
/**
|
|
68
|
+
* The current collections, readable outside a state updater.
|
|
69
|
+
*
|
|
70
|
+
* Three mutators used to compute their new value inside
|
|
71
|
+
* `setCollections(prev => …)` and persist from in there, which cost them
|
|
72
|
+
* both halves of a save: the promise they returned resolved before the
|
|
73
|
+
* store had answered, so a failed write reached the caller as a success,
|
|
74
|
+
* and a `setState` updater must be pure — under `StrictMode` React invokes
|
|
75
|
+
* it twice, so every one of those saves wrote to the store twice in
|
|
76
|
+
* development. Reading `prev` from a ref lets them do what
|
|
77
|
+
* `updatePropertiesOrder` already did: compute, set, then await.
|
|
78
|
+
*/
|
|
79
|
+
const collectionsRef = useRef<AdminCollection[]>([]);
|
|
80
|
+
const applyCollections = useCallback((next: AdminCollection[]) => {
|
|
81
|
+
collectionsRef.current = next;
|
|
82
|
+
setCollections(next);
|
|
83
|
+
}, []);
|
|
84
|
+
|
|
67
85
|
// ── Load on mount ─────────────────────────────────────────────────
|
|
68
86
|
useEffect(() => {
|
|
69
87
|
if (!autoLoad) return;
|
|
@@ -78,7 +96,7 @@ export function useJsonCollectionsConfigController({
|
|
|
78
96
|
if (cancelled) return;
|
|
79
97
|
// Deserialized as a set: a relation's target is a slug, and a slug only
|
|
80
98
|
// resolves against the other collections it arrived with.
|
|
81
|
-
|
|
99
|
+
applyCollections(fromSerializableCollectionConfigs(serialized));
|
|
82
100
|
setNavigationEntries(navEntries);
|
|
83
101
|
} catch (e) {
|
|
84
102
|
console.error("useJsonCollectionsConfigController: failed to load collections", e);
|
|
@@ -88,7 +106,10 @@ export function useJsonCollectionsConfigController({
|
|
|
88
106
|
})();
|
|
89
107
|
|
|
90
108
|
return () => { cancelled = true; };
|
|
91
|
-
|
|
109
|
+
// `applyCollections` is `useCallback(…, [])`, so naming it here cannot
|
|
110
|
+
// re-run the mount effect — it is listed because a stable identity is a
|
|
111
|
+
// fact about today's code, not a reason to leave a dependency out.
|
|
112
|
+
}, [autoLoad, applyCollections]);
|
|
92
113
|
|
|
93
114
|
// ── Helpers ────────────────────────────────────────────────────────
|
|
94
115
|
|
|
@@ -116,49 +137,44 @@ export function useJsonCollectionsConfigController({
|
|
|
116
137
|
) => {
|
|
117
138
|
const collection = collectionData as AdminCollection;
|
|
118
139
|
await persistCollection(collection);
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
}
|
|
128
|
-
}, [persistCollection]);
|
|
140
|
+
const prev = collectionsRef.current;
|
|
141
|
+
const idx = prev.findIndex(c => c.slug === id);
|
|
142
|
+
if (idx >= 0) {
|
|
143
|
+
const next = [...prev];
|
|
144
|
+
next[idx] = collection;
|
|
145
|
+
applyCollections(next);
|
|
146
|
+
} else {
|
|
147
|
+
applyCollections([...prev, collection]);
|
|
148
|
+
}
|
|
149
|
+
}, [persistCollection, applyCollections]);
|
|
129
150
|
|
|
130
151
|
const updateCollection = useCallback(async <M extends Record<string, unknown>>(
|
|
131
152
|
{ id, collectionData, previousId }: UpdateCollectionParams<M>
|
|
132
153
|
) => {
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
return next;
|
|
155
|
-
});
|
|
156
|
-
}, [persistCollection]);
|
|
154
|
+
const prev = collectionsRef.current;
|
|
155
|
+
const lookupId = previousId ?? id;
|
|
156
|
+
const idx = prev.findIndex(c => c.slug === lookupId);
|
|
157
|
+
if (idx < 0) return;
|
|
158
|
+
|
|
159
|
+
const merged = { ...prev[idx], ...collectionData } as AdminCollection;
|
|
160
|
+
const next = [...prev];
|
|
161
|
+
next[idx] = merged;
|
|
162
|
+
applyCollections(next);
|
|
163
|
+
|
|
164
|
+
await persistCollection(merged);
|
|
165
|
+
|
|
166
|
+
// If the slug changed, clean up the old entry in the store. After the
|
|
167
|
+
// save, not beside it: dropping the old key first and then failing to
|
|
168
|
+
// write the new one loses the collection outright.
|
|
169
|
+
if (previousId && previousId !== id) {
|
|
170
|
+
await storeRef.current.delete(previousId);
|
|
171
|
+
}
|
|
172
|
+
}, [persistCollection, applyCollections]);
|
|
157
173
|
|
|
158
174
|
const deleteCollection = useCallback(async ({ id }: DeleteCollectionParams) => {
|
|
159
175
|
await storeRef.current.delete(id);
|
|
160
|
-
|
|
161
|
-
}, []);
|
|
176
|
+
applyCollections(collectionsRef.current.filter(c => c.slug !== id));
|
|
177
|
+
}, [applyCollections]);
|
|
162
178
|
|
|
163
179
|
const saveProperty = useCallback(async ({
|
|
164
180
|
path,
|
|
@@ -166,56 +182,48 @@ export function useJsonCollectionsConfigController({
|
|
|
166
182
|
property,
|
|
167
183
|
newPropertiesOrder,
|
|
168
184
|
}: SavePropertyParams) => {
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
const next = [...prev];
|
|
183
|
-
next[idx] = collection as AdminCollection;
|
|
185
|
+
const prev = collectionsRef.current;
|
|
186
|
+
const idx = prev.findIndex(c => c.slug === path);
|
|
187
|
+
if (idx < 0) return;
|
|
188
|
+
|
|
189
|
+
const collection = { ...prev[idx] };
|
|
190
|
+
collection.properties = {
|
|
191
|
+
...collection.properties,
|
|
192
|
+
[propertyKey]: property,
|
|
193
|
+
} as EngineProperties;
|
|
194
|
+
if (newPropertiesOrder) {
|
|
195
|
+
collection.propertiesOrder = newPropertiesOrder as typeof collection.propertiesOrder;
|
|
196
|
+
}
|
|
184
197
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
198
|
+
const next = [...prev];
|
|
199
|
+
next[idx] = collection as AdminCollection;
|
|
200
|
+
applyCollections(next);
|
|
188
201
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
}, [persistCollection]);
|
|
202
|
+
await persistCollection(next[idx]);
|
|
203
|
+
}, [persistCollection, applyCollections]);
|
|
192
204
|
|
|
193
205
|
const deleteProperty = useCallback(async ({
|
|
194
206
|
path,
|
|
195
207
|
propertyKey,
|
|
196
208
|
newPropertiesOrder,
|
|
197
209
|
}: DeletePropertyParams) => {
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
const next = [...prev];
|
|
210
|
-
next[idx] = collection as AdminCollection;
|
|
210
|
+
const prev = collectionsRef.current;
|
|
211
|
+
const idx = prev.findIndex(c => c.slug === path);
|
|
212
|
+
if (idx < 0) return;
|
|
213
|
+
|
|
214
|
+
const collection = { ...prev[idx] };
|
|
215
|
+
const { [propertyKey]: _removed, ...remaining } = collection.properties;
|
|
216
|
+
collection.properties = remaining;
|
|
217
|
+
if (newPropertiesOrder) {
|
|
218
|
+
collection.propertiesOrder = newPropertiesOrder as typeof collection.propertiesOrder;
|
|
219
|
+
}
|
|
211
220
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
221
|
+
const next = [...prev];
|
|
222
|
+
next[idx] = collection as AdminCollection;
|
|
223
|
+
applyCollections(next);
|
|
215
224
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
}, [persistCollection]);
|
|
225
|
+
await persistCollection(next[idx]);
|
|
226
|
+
}, [persistCollection, applyCollections]);
|
|
219
227
|
|
|
220
228
|
const updatePropertiesOrder = useCallback(async ({
|
|
221
229
|
collection,
|
|
@@ -226,16 +234,16 @@ export function useJsonCollectionsConfigController({
|
|
|
226
234
|
propertiesOrder: newPropertiesOrder,
|
|
227
235
|
} as AdminCollection;
|
|
228
236
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
237
|
+
const prev = collectionsRef.current;
|
|
238
|
+
const idx = prev.findIndex(c => c.slug === collection.slug);
|
|
239
|
+
if (idx >= 0) {
|
|
232
240
|
const next = [...prev];
|
|
233
241
|
next[idx] = updated;
|
|
234
|
-
|
|
235
|
-
}
|
|
242
|
+
applyCollections(next);
|
|
243
|
+
}
|
|
236
244
|
|
|
237
245
|
await persistCollection(updated);
|
|
238
|
-
}, [persistCollection]);
|
|
246
|
+
}, [persistCollection, applyCollections]);
|
|
239
247
|
|
|
240
248
|
const updateKanbanColumnsOrder = useCallback(async ({
|
|
241
249
|
collection,
|
|
@@ -202,13 +202,18 @@ collectionData });
|
|
|
202
202
|
await request("/collection/delete", { collectionId: id });
|
|
203
203
|
},
|
|
204
204
|
|
|
205
|
+
// The follow-up write carries exactly one key, and `partial` is what says
|
|
206
|
+
// so. Read as a whole-collection save it deletes everything it does not
|
|
207
|
+
// mention — `securityRules` first, which then falls back to the
|
|
208
|
+
// directory default and widens who can read the collection.
|
|
205
209
|
saveProperty: async ({ path, propertyKey, property, newPropertiesOrder }: SavePropertyParams) => {
|
|
206
210
|
await request("/property/save", { collectionId: path,
|
|
207
211
|
propertyKey,
|
|
208
212
|
propertyConfig: property });
|
|
209
213
|
if (newPropertiesOrder) {
|
|
210
214
|
await request("/collection/save", { collectionId: path,
|
|
211
|
-
collectionData: { propertiesOrder: newPropertiesOrder }
|
|
215
|
+
collectionData: { propertiesOrder: newPropertiesOrder },
|
|
216
|
+
partial: true });
|
|
212
217
|
}
|
|
213
218
|
},
|
|
214
219
|
deleteProperty: async ({ path, propertyKey, newPropertiesOrder }: DeletePropertyParams) => {
|
|
@@ -216,14 +221,16 @@ collectionData: { propertiesOrder: newPropertiesOrder } });
|
|
|
216
221
|
propertyKey });
|
|
217
222
|
if (newPropertiesOrder) {
|
|
218
223
|
await request("/collection/save", { collectionId: path,
|
|
219
|
-
collectionData: { propertiesOrder: newPropertiesOrder }
|
|
224
|
+
collectionData: { propertiesOrder: newPropertiesOrder },
|
|
225
|
+
partial: true });
|
|
220
226
|
}
|
|
221
227
|
},
|
|
222
228
|
|
|
223
229
|
updatePropertiesOrder: async ({ collection, fullPath, newPropertiesOrder }: UpdatePropertiesOrderParams) => {
|
|
224
230
|
const collectionId = (collection as AdminCollection & { id?: string }).id || fullPath.split("/").pop();
|
|
225
231
|
await request("/collection/save", { collectionId,
|
|
226
|
-
collectionData: { propertiesOrder: newPropertiesOrder }
|
|
232
|
+
collectionData: { propertiesOrder: newPropertiesOrder },
|
|
233
|
+
partial: true });
|
|
227
234
|
},
|
|
228
235
|
updateKanbanColumnsOrder: async () => {
|
|
229
236
|
// Kanban order mapping logic can be added later if needed natively.
|
|
@@ -1,13 +1,20 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { FilterXIcon, iconSize, IconButton, Tooltip } from "@rebasepro/ui";
|
|
2
2
|
import { EntityTableController } from "@rebasepro/admin-types";
|
|
3
3
|
import { useTranslation } from "@rebasepro/app";
|
|
4
4
|
|
|
5
5
|
export function ClearFilterSortButton({
|
|
6
6
|
tableController,
|
|
7
|
-
enabled
|
|
7
|
+
enabled,
|
|
8
|
+
compact
|
|
8
9
|
}: {
|
|
9
10
|
enabled: boolean;
|
|
10
|
-
tableController: EntityTableController
|
|
11
|
+
tableController: EntityTableController;
|
|
12
|
+
/**
|
|
13
|
+
* Toolbar showing icon-only controls (split view, or a layout too narrow
|
|
14
|
+
* for the filters button's label). Only decides the icon size, which
|
|
15
|
+
* follows that button — see `CollectionViewStartActions`.
|
|
16
|
+
*/
|
|
17
|
+
compact?: boolean;
|
|
11
18
|
}) {
|
|
12
19
|
const { t } = useTranslation();
|
|
13
20
|
|
|
@@ -29,17 +36,19 @@ export function ClearFilterSortButton({
|
|
|
29
36
|
}
|
|
30
37
|
return (
|
|
31
38
|
<Tooltip title={label}>
|
|
32
|
-
|
|
39
|
+
{/* An `IconButton`, like every other icon-only control in this
|
|
40
|
+
toolbar. As a `Button` it was a rounded-md box with text
|
|
41
|
+
padding sitting between two circular icon buttons. */}
|
|
42
|
+
<IconButton
|
|
33
43
|
size={"small"}
|
|
34
|
-
variant={"text"}
|
|
35
44
|
aria-label={label}
|
|
36
45
|
onClick={() => {
|
|
37
46
|
tableController.clearFilter?.();
|
|
38
47
|
tableController.setSortBy?.(undefined);
|
|
39
48
|
}}
|
|
40
49
|
>
|
|
41
|
-
<FilterXIcon size={iconSize.small}/>
|
|
42
|
-
</
|
|
50
|
+
<FilterXIcon size={compact ? iconSize.smallest : iconSize.small}/>
|
|
51
|
+
</IconButton>
|
|
43
52
|
</Tooltip>
|
|
44
53
|
);
|
|
45
54
|
}
|
|
@@ -240,7 +240,13 @@ export function getTableBindingForProperty(
|
|
|
240
240
|
Component: RelationDialogBindingComponent,
|
|
241
241
|
allowScroll: false
|
|
242
242
|
};
|
|
243
|
-
} else {
|
|
243
|
+
} else if (selected) {
|
|
244
|
+
// Gated on `selected`, like the number and string editors above.
|
|
245
|
+
// This one mounts a live picker against the *target* collection,
|
|
246
|
+
// and the cell is rendered once per visible row: ungated, opening
|
|
247
|
+
// a table with one relation column fired a request — or, on the
|
|
248
|
+
// realtime path, a subscription — per row, for data nobody had
|
|
249
|
+
// asked to see. Unselected cells fall through to PropertyPreview.
|
|
244
250
|
return {
|
|
245
251
|
Component: RelationSelectorBindingComponent,
|
|
246
252
|
allowScroll: false
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
|
|
2
2
|
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
3
|
-
import { Entity, EnumValueConfig } from "@rebasepro/types";
|
|
3
|
+
import { Entity, EnumValueConfig, MAX_LIST_LIMIT } from "@rebasepro/types";
|
|
4
4
|
import { EntityTableController, SelectionController, AdminCollection } from "@rebasepro/admin-types";
|
|
5
5
|
import { BoardCardBinding } from "./BoardCardBinding";
|
|
6
6
|
import {
|
|
@@ -376,13 +376,19 @@ parentEntityIds,
|
|
|
376
376
|
setBackfillLoading(true);
|
|
377
377
|
|
|
378
378
|
try {
|
|
379
|
-
// Fetch
|
|
380
|
-
//
|
|
381
|
-
//
|
|
382
|
-
//
|
|
383
|
-
//
|
|
379
|
+
// Fetch the documents to key (not relying on loaded entities). No
|
|
380
|
+
// `orderBy`: with no usable order key there is nothing to order by,
|
|
381
|
+
// and this is the same query the columns themselves run, so the
|
|
382
|
+
// keys are handed out in the order the board is showing — which is
|
|
383
|
+
// what the dialog promises.
|
|
384
|
+
//
|
|
385
|
+
// `MAX_LIST_LIMIT`, not a made-up 10 000: that is the most a single
|
|
386
|
+
// read serves, and asking for more is now refused outright rather
|
|
387
|
+
// than quietly answered with the first thousand. A board past the
|
|
388
|
+
// ceiling still gets only its first page backfilled — the same rows
|
|
389
|
+
// as before this became explicit — and wants a paged walk.
|
|
384
390
|
const allDocsRes = await dataClient.collection(fullPath).find({
|
|
385
|
-
limit:
|
|
391
|
+
limit: MAX_LIST_LIMIT
|
|
386
392
|
});
|
|
387
393
|
const allDocs = allDocsRes.data as Entity<M>[];
|
|
388
394
|
|
|
@@ -104,6 +104,13 @@ parentEntityIds,
|
|
|
104
104
|
</Tooltip>
|
|
105
105
|
);
|
|
106
106
|
|
|
107
|
+
// Whether the toolbar is showing icon-only controls. The filters button
|
|
108
|
+
// drops its label — and its icon a size with it — on a narrow layout as
|
|
109
|
+
// well as in the split view, and the sort and clear controls beside it
|
|
110
|
+
// follow: keyed on `compact` alone they stayed 20px next to a 16px filter
|
|
111
|
+
// icon at every width below `largeLayout`.
|
|
112
|
+
const iconOnlyToolbar = !largeLayout || Boolean(compact);
|
|
113
|
+
|
|
107
114
|
const filtersButton = resolvedProperties && tableController.setFilterValues && (
|
|
108
115
|
<Tooltip title={t("filters")}
|
|
109
116
|
key={"filters_tooltip"}>
|
|
@@ -147,6 +154,7 @@ parentEntityIds,
|
|
|
147
154
|
key={"sort_button"}
|
|
148
155
|
tableController={tableController}
|
|
149
156
|
properties={resolvedProperties}
|
|
157
|
+
compact={iconOnlyToolbar}
|
|
150
158
|
/>
|
|
151
159
|
);
|
|
152
160
|
|
|
@@ -170,6 +178,7 @@ parentEntityIds,
|
|
|
170
178
|
<ClearFilterSortButton
|
|
171
179
|
key={"clear_filter"}
|
|
172
180
|
tableController={tableController}
|
|
181
|
+
compact={iconOnlyToolbar}
|
|
173
182
|
enabled={!collection.fixedFilter}/>,
|
|
174
183
|
filterPresetsButton
|
|
175
184
|
];
|