@rebasepro/admin 0.13.1-canary.gef9608c → 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.
Files changed (148) hide show
  1. package/dist/{CollectionEditorDialog-7b-kSLno.js → CollectionEditorDialog-CSheud_E.js} +19 -9
  2. package/dist/CollectionEditorDialog-CSheud_E.js.map +1 -0
  3. package/dist/{PropertyEditView-C-31uvpG.js → PropertyEditView-CWZlC1gq.js} +5 -5
  4. package/dist/PropertyEditView-CWZlC1gq.js.map +1 -0
  5. package/dist/{RouterCollectionsStudioView-BJylABq3.js → RouterCollectionsStudioView-CwTpdzWF.js} +9 -13
  6. package/dist/RouterCollectionsStudioView-CwTpdzWF.js.map +1 -0
  7. package/dist/collection_editor/serializable_types.d.ts +1 -0
  8. package/dist/collection_editor_ui.js +4 -4
  9. package/dist/components/ClearFilterSortButton.d.ts +7 -1
  10. package/dist/components/CollectionViewBinding/EntityCardBinding.d.ts +3 -1
  11. package/dist/components/CollectionViewBinding/SearchExplanation.d.ts +48 -0
  12. package/dist/components/CollectionViewBinding/SearchHighlight.d.ts +101 -0
  13. package/dist/components/CollectionViewBinding/SlotValue.d.ts +26 -0
  14. package/dist/components/CollectionViewBinding/SortButton.d.ts +7 -1
  15. package/dist/components/CollectionViewBinding/SplitListCloseButton.d.ts +17 -6
  16. package/dist/components/CollectionViewBinding/SplitListShowButton.d.ts +1 -1
  17. package/dist/components/CollectionViewBinding/usePreviewSlots.d.ts +26 -1
  18. package/dist/components/DetailViewBinding.d.ts +7 -0
  19. package/dist/components/DrawerNavigationGroup.d.ts +4 -2
  20. package/dist/components/DrawerNavigationItem.d.ts +13 -5
  21. package/dist/components/EditViewBinding.d.ts +22 -7
  22. package/dist/components/EntityIdentityBar.d.ts +24 -1
  23. package/dist/components/RelationSelector.d.ts +13 -0
  24. package/dist/data_export/export/export.d.ts +7 -0
  25. package/dist/data_export/export/fetch_export_data.d.ts +48 -0
  26. package/dist/data_export/export/index.d.ts +1 -0
  27. package/dist/data_import/utils/csv.d.ts +30 -0
  28. package/dist/data_import/utils/index.d.ts +2 -0
  29. package/dist/data_import/utils/save_entities.d.ts +38 -0
  30. package/dist/data_import/utils/transforms.d.ts +9 -0
  31. package/dist/editor.js +3 -0
  32. package/dist/editor.js.map +1 -1
  33. package/dist/{export-C-XG3aRh.js → export-L53WektO.js} +175 -39
  34. package/dist/export-L53WektO.js.map +1 -0
  35. package/dist/form/form_utils.d.ts +38 -1
  36. package/dist/{history-DfMuvPfr.js → history-D5SKygpJ.js} +2 -2
  37. package/dist/{history-DfMuvPfr.js.map → history-D5SKygpJ.js.map} +1 -1
  38. package/dist/{import-B0GyllB0.js → import-C_4edkoD.js} +5 -4
  39. package/dist/import-C_4edkoD.js.map +1 -0
  40. package/dist/index.d.ts +2 -2
  41. package/dist/index.js +96 -80
  42. package/dist/index.js.map +1 -1
  43. package/dist/preview/components/date_fns_locales.d.ts +9 -0
  44. package/dist/util/view_constants.d.ts +24 -0
  45. package/dist/util/view_mode.d.ts +18 -0
  46. package/dist/{util-DHNXodBo.js → util-C-D5yD2n.js} +2173 -673
  47. package/dist/util-C-D5yD2n.js.map +1 -0
  48. package/package.json +10 -9
  49. package/src/collection_editor/serializable_types.ts +1 -0
  50. package/src/collection_editor/ui/collection_editor/AICollectionGeneratorPopover.tsx +8 -3
  51. package/src/collection_editor/ui/collection_editor/CollectionDetailsForm.tsx +5 -2
  52. package/src/collection_editor/ui/collection_editor/CollectionJsonImportDialog.tsx +1 -0
  53. package/src/collection_editor/ui/collection_editor/CollectionRLSTab.tsx +9 -8
  54. package/src/collection_editor/ui/collection_editor/CollectionsStudioView.tsx +9 -11
  55. package/src/collection_editor/ui/collection_editor/GeneralSettingsForm.tsx +5 -2
  56. package/src/collection_editor/ui/collection_editor/properties/MapPropertyField.tsx +1 -1
  57. package/src/collection_editor/ui/collection_editor/properties/advanced/AdvancedPropertyValidation.tsx +5 -2
  58. package/src/collection_editor/ui/collection_editor/properties/conditions/ConditionsEditor.tsx +1 -0
  59. package/src/collection_editor/useJsonCollectionsConfigController.tsx +92 -84
  60. package/src/collection_editor/useLocalCollectionsConfigController.tsx +10 -3
  61. package/src/components/ClearFilterSortButton.tsx +16 -7
  62. package/src/components/CollectionTableBinding/CollectionTableBinding.tsx +2 -2
  63. package/src/components/CollectionTableBinding/PropertyTableCell.tsx +2 -2
  64. package/src/components/CollectionTableBinding/column_utils.tsx +15 -6
  65. package/src/components/CollectionTableBinding/internal/popup_field/PopupFormField.tsx +2 -2
  66. package/src/components/CollectionTableBinding/table_bindings.tsx +7 -1
  67. package/src/components/CollectionViewBinding/BoardCardBinding.tsx +18 -1
  68. package/src/components/CollectionViewBinding/CollectionBoardViewBinding.tsx +13 -7
  69. package/src/components/CollectionViewBinding/CollectionCardViewBinding.tsx +2 -1
  70. package/src/components/CollectionViewBinding/CollectionListViewBinding.tsx +816 -335
  71. package/src/components/CollectionViewBinding/CollectionViewActions.tsx +1 -1
  72. package/src/components/CollectionViewBinding/CollectionViewBinding.tsx +26 -6
  73. package/src/components/CollectionViewBinding/CollectionViewStartActions.tsx +9 -0
  74. package/src/components/CollectionViewBinding/EntityCardBinding.tsx +45 -10
  75. package/src/components/CollectionViewBinding/SearchExplanation.tsx +106 -0
  76. package/src/components/CollectionViewBinding/SearchHighlight.tsx +311 -0
  77. package/src/components/CollectionViewBinding/SlotValue.tsx +96 -0
  78. package/src/components/CollectionViewBinding/SortButton.tsx +16 -4
  79. package/src/components/CollectionViewBinding/SplitListCloseButton.tsx +23 -9
  80. package/src/components/CollectionViewBinding/SplitListShowButton.tsx +3 -3
  81. package/src/components/CollectionViewBinding/SplitListView.tsx +36 -9
  82. package/src/components/CollectionViewBinding/ViewModeToggle.tsx +10 -5
  83. package/src/components/CollectionViewBinding/useBoardDataController.tsx +11 -3
  84. package/src/components/CollectionViewBinding/usePreviewSlots.ts +76 -24
  85. package/src/components/CollectionViewBinding/utils.ts +7 -4
  86. package/src/components/DefaultDrawer.tsx +7 -0
  87. package/src/components/DetailViewBinding.tsx +33 -9
  88. package/src/components/DrawerNavigationGroup.tsx +12 -32
  89. package/src/components/DrawerNavigationItem.tsx +33 -12
  90. package/src/components/EditViewBinding.tsx +175 -28
  91. package/src/components/EntityIdentityBar.tsx +109 -17
  92. package/src/components/EntityInspector.tsx +24 -6
  93. package/src/components/EntityViewBinding.tsx +4 -2
  94. package/src/components/ReferenceTable/SelectionTableBinding.tsx +4 -2
  95. package/src/components/RelationSelector.tsx +61 -11
  96. package/src/components/SearchIconsView.tsx +14 -33
  97. package/src/components/SelectableTable/filters/StringNumberFilterField.tsx +1 -0
  98. package/src/components/SidePanelBinding.tsx +36 -25
  99. package/src/components/UserSelector.tsx +52 -9
  100. package/src/components/history/LastEditedByIndicator.tsx +7 -12
  101. package/src/data_export/export/ExportCollectionAction.tsx +101 -48
  102. package/src/data_export/export/export.ts +63 -16
  103. package/src/data_export/export/fetch_export_data.ts +90 -0
  104. package/src/data_export/export/index.ts +1 -0
  105. package/src/data_import/components/DataNewPropertiesMapping.tsx +20 -3
  106. package/src/data_import/components/ImportNewPropertyFieldPreview.tsx +1 -0
  107. package/src/data_import/components/ImportSaveInProgress.tsx +35 -36
  108. package/src/data_import/import/ImportCollectionAction.tsx +13 -2
  109. package/src/data_import/utils/csv.ts +153 -0
  110. package/src/data_import/utils/data.ts +16 -2
  111. package/src/data_import/utils/file_to_json.ts +77 -6
  112. package/src/data_import/utils/index.ts +2 -0
  113. package/src/data_import/utils/save_entities.ts +138 -0
  114. package/src/data_import/utils/transforms.ts +23 -0
  115. package/src/editor/components/image-bubble.tsx +2 -0
  116. package/src/editor/selectors/link-selector.tsx +1 -0
  117. package/src/form/EntityForm.tsx +12 -3
  118. package/src/form/EntityFormBinding.tsx +8 -12
  119. package/src/form/PropertyFieldBinding.tsx +2 -2
  120. package/src/form/field_bindings/ArrayCustomShapedFieldBinding.tsx +2 -2
  121. package/src/form/field_bindings/BinaryFieldBinding.tsx +1 -0
  122. package/src/form/field_bindings/BlockFieldBinding.tsx +3 -3
  123. package/src/form/field_bindings/DateTimeFieldBinding.tsx +4 -0
  124. package/src/form/field_bindings/KeyValueFieldBinding.tsx +3 -0
  125. package/src/form/field_bindings/MapFieldBinding.tsx +2 -2
  126. package/src/form/field_bindings/ReadOnlyFieldBinding.tsx +15 -2
  127. package/src/form/field_bindings/RepeatFieldBinding.tsx +3 -3
  128. package/src/form/field_bindings/StorageUploadFieldBinding.tsx +2 -2
  129. package/src/form/field_bindings/TextFieldBinding.tsx +18 -0
  130. package/src/form/field_bindings/VectorFieldBinding.tsx +1 -0
  131. package/src/form/form_utils.ts +78 -0
  132. package/src/index.ts +9 -1
  133. package/src/preview/PropertyPreview.tsx +7 -3
  134. package/src/preview/components/DatePreview.tsx +15 -2
  135. package/src/preview/components/ReferencePreview.tsx +5 -8
  136. package/src/preview/components/RelationPreview.tsx +8 -7
  137. package/src/preview/components/date_fns_locales.ts +125 -0
  138. package/src/preview/property_previews/ArrayOfRelationsPreview.tsx +6 -3
  139. package/src/util/navigation_utils.ts +4 -2
  140. package/src/util/property_utils.tsx +6 -1
  141. package/src/util/view_constants.ts +27 -0
  142. package/src/util/view_mode.ts +36 -0
  143. package/dist/CollectionEditorDialog-7b-kSLno.js.map +0 -1
  144. package/dist/PropertyEditView-C-31uvpG.js.map +0 -1
  145. package/dist/RouterCollectionsStudioView-BJylABq3.js.map +0 -1
  146. package/dist/export-C-XG3aRh.js.map +0 -1
  147. package/dist/import-B0GyllB0.js.map +0 -1
  148. package/dist/util-DHNXodBo.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.13.1-canary.gef9608c",
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/admin-types": "0.13.1-canary.gef9608c",
87
- "@rebasepro/app": "0.13.1-canary.gef9608c",
88
- "@rebasepro/forms": "0.13.1-canary.gef9608c",
89
- "@rebasepro/common": "0.13.1-canary.gef9608c",
90
- "@rebasepro/inference": "0.13.1-canary.gef9608c",
91
- "@rebasepro/ui": "0.13.1-canary.gef9608c",
92
- "@rebasepro/types": "0.13.1-canary.gef9608c",
93
- "@rebasepro/utils": "0.13.1-canary.gef9608c"
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",
@@ -155,6 +155,7 @@ export interface SerializableAdminRelationOptions extends SerializableAdminBaseO
155
155
  fixedFilter?: FilterValues<string>;
156
156
  includeId?: boolean;
157
157
  includeEntityLink?: boolean;
158
+ renderInForm?: boolean;
158
159
  }
159
160
 
160
161
  /** JSON-serializable version of `AdminArrayOptions`. */
@@ -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
- ? "Describe the changes you want to make to this collection."
208
- : "Describe the collection you want to create."
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
- return <Tooltip title={"Database ID"}
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 ?? ""}
@@ -110,6 +110,7 @@ export function CollectionJsonImportDialog({
110
110
  <TextField
111
111
  multiline
112
112
  minRows={12}
113
+ aria-label="Collection JSON"
113
114
  value={jsonValue}
114
115
  onChange={handleJsonChange}
115
116
  placeholder={EXAMPLE_JSON}
@@ -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">Policy Name</Typography>
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">USING expression</Typography>
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">WITH CHECK expression</Typography>
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>
@@ -17,6 +17,7 @@ import { CollectionsConfigController } from "../../types/config_controller";
17
17
  import { CollectionStudioView } from "./CollectionStudioView";
18
18
  import type { CollectionEditorExtensionProps } from "../../extensibility_types";
19
19
  import type { AdminCollection } from "@rebasepro/admin-types";
20
+ import { readStoredString, writeStoredString } from "@rebasepro/utils";
20
21
 
21
22
  export interface CollectionsStudioViewProps extends CollectionEditorExtensionProps {
22
23
  configController: CollectionsConfigController;
@@ -77,21 +78,18 @@ export function CollectionsStudioView({
77
78
  const collections = collectionsProp ?? configController.collections ?? [];
78
79
 
79
80
  // ── Sidebar sizing ──────────────────────────────────────────────────
81
+ // Checked, not just parsed: `parseFloat` answers NaN for anything it cannot
82
+ // read, and a NaN pane size lays the sidebar out to nothing — with the bad
83
+ // value still in storage on reload. `SplitListView.getSavedPanelSize` is the
84
+ // same read, done this way.
80
85
  const [sidebarSize, setSidebarSize] = useState(() => {
81
- try {
82
- const saved = localStorage.getItem("rebase_collections_editor_sidebar_size");
83
- return saved !== null ? parseFloat(saved) : 25;
84
- } catch (e) {
85
- return 25;
86
- }
86
+ const saved = readStoredString("rebase_collections_editor_sidebar_size");
87
+ const parsed = saved === null ? NaN : parseFloat(saved);
88
+ return Number.isFinite(parsed) && parsed > 0 && parsed < 100 ? parsed : 25;
87
89
  });
88
90
 
89
91
  useEffect(() => {
90
- try {
91
- localStorage.setItem("rebase_collections_editor_sidebar_size", sidebarSize.toString());
92
- } catch (e) {
93
- // ignore local storage error
94
- }
92
+ writeStoredString("rebase_collections_editor_sidebar_size", sidebarSize.toString());
95
93
  }, [sidebarSize]);
96
94
 
97
95
  return (
@@ -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
- return <Tooltip title={"Database ID"}
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("ui.spreadChildren", v)}
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
- const hideFromCollection = "ui.hideFromCollection";
11
- const readOnly = "ui.readOnly";
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
 
@@ -302,6 +302,7 @@ function ConditionValueInput({
302
302
  type="number"
303
303
  size="small"
304
304
  disabled={disabled}
305
+ aria-label="Condition value"
305
306
  placeholder="Value"
306
307
  className="flex-1 min-w-[80px]"
307
308
  onChange={(e) => onChange(e.target.value)}
@@ -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
- setCollections(fromSerializableCollectionConfigs(serialized));
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
- }, [autoLoad]);
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
- setCollections(prev => {
120
- const idx = prev.findIndex(c => c.slug === id);
121
- if (idx >= 0) {
122
- const next = [...prev];
123
- next[idx] = collection;
124
- return next;
125
- }
126
- return [...prev, collection];
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
- setCollections(prev => {
134
- const lookupId = previousId ?? id;
135
- const idx = prev.findIndex(c => c.slug === lookupId);
136
- if (idx < 0) return prev;
137
-
138
- const merged = { ...prev[idx], ...collectionData } as AdminCollection;
139
- const next = [...prev];
140
- next[idx] = merged;
141
-
142
- // Persist async — fire-and-forget with error logging
143
- persistCollection(merged).catch(e =>
144
- console.error("useJsonCollectionsConfigController: failed to update collection", e)
145
- );
146
-
147
- // If slug changed, clean up the old entry in the store
148
- if (previousId && previousId !== id) {
149
- storeRef.current.delete(previousId).catch(e =>
150
- console.error("useJsonCollectionsConfigController: failed to delete old slug", e)
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
- setCollections(prev => prev.filter(c => c.slug !== id));
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
- setCollections(prev => {
170
- const idx = prev.findIndex(c => c.slug === path);
171
- if (idx < 0) return prev;
172
-
173
- const collection = { ...prev[idx] };
174
- collection.properties = {
175
- ...collection.properties,
176
- [propertyKey]: property,
177
- } as EngineProperties;
178
- if (newPropertiesOrder) {
179
- collection.propertiesOrder = newPropertiesOrder as typeof collection.propertiesOrder;
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
- persistCollection(next[idx]).catch(e =>
186
- console.error("useJsonCollectionsConfigController: failed to save property", e)
187
- );
198
+ const next = [...prev];
199
+ next[idx] = collection as AdminCollection;
200
+ applyCollections(next);
188
201
 
189
- return next;
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
- setCollections(prev => {
199
- const idx = prev.findIndex(c => c.slug === path);
200
- if (idx < 0) return prev;
201
-
202
- const collection = { ...prev[idx] };
203
- const { [propertyKey]: _removed, ...remaining } = collection.properties;
204
- collection.properties = remaining;
205
- if (newPropertiesOrder) {
206
- collection.propertiesOrder = newPropertiesOrder as typeof collection.propertiesOrder;
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
- persistCollection(next[idx]).catch(e =>
213
- console.error("useJsonCollectionsConfigController: failed to delete property", e)
214
- );
221
+ const next = [...prev];
222
+ next[idx] = collection as AdminCollection;
223
+ applyCollections(next);
215
224
 
216
- return next;
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
- setCollections(prev => {
230
- const idx = prev.findIndex(c => c.slug === collection.slug);
231
- if (idx < 0) return prev;
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
- return next;
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 { Button, FilterXIcon, iconSize, Tooltip } from "@rebasepro/ui";
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
- <Button
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
- </Button>
50
+ <FilterXIcon size={compact ? iconSize.smallest : iconSize.small}/>
51
+ </IconButton>
43
52
  </Tooltip>
44
53
  );
45
54
  }
@@ -17,7 +17,7 @@ import { propertiesToColumns } from "./column_utils";
17
17
  import { ErrorView } from "@rebasepro/app";
18
18
  import { SelectableTable } from "../SelectableTable/SelectableTable";
19
19
  import { cls } from "@rebasepro/ui";
20
- import { getRowHeight } from "@rebasepro/app";
20
+ import { getRowHeight, isDisabled } from "@rebasepro/app";
21
21
  import { getValueInPath } from "@rebasepro/utils";
22
22
 
23
23
  /**
@@ -147,7 +147,7 @@ export const CollectionTableBinding = function CollectionTableBinding<M extends
147
147
  propertyKey,
148
148
  entity
149
149
  }) ?? columnCustom?.resolvedProperty;
150
- if (!property?.admin?.disabled) {
150
+ if (!property || !isDisabled(property)) {
151
151
  disabled = false;
152
152
  }
153
153
 
@@ -18,7 +18,7 @@ import { EntityTableCellActions } from "./internal/EntityTableCellActions";
18
18
  import { useSelectableTableController } from "../SelectableTable/SelectableTableContext";
19
19
  import { useClearRestoreValue } from "../../form/useClearRestoreValue";
20
20
  import { getRowHeight } from "@rebasepro/app";
21
- import { isReadOnly } from "@rebasepro/app";
21
+ import { isDisabled, isReadOnly } from "@rebasepro/app";
22
22
  import { TableRelationField } from "./fields/TableRelationField";
23
23
  import { TableRelationSelectorField } from "./fields/TableRelationSelectorField";
24
24
 
@@ -95,7 +95,7 @@ export const PropertyTableCell = React.memo<PropertyTableCellProps<any>>(
95
95
  const customPreview = Boolean(property.admin?.Preview);
96
96
  const readOnlyProperty = isReadOnly(property);
97
97
  const disabledTooltip: string | undefined = typeof property.admin?.disabled === "object" ? property.admin?.disabled.disabledMessage : undefined;
98
- const disabled = readonly || disabledProp || Boolean(property.admin?.disabled);
98
+ const disabled = readonly || disabledProp || isDisabled(property);
99
99
 
100
100
  const validation = useMemo(() => mapPropertyToZod({
101
101
  property,